Skip to content

feat(particle): support continuous Birth sub-emitters#3071

Open
hhhhkrx wants to merge 16 commits into
galacean:dev/2.0from
hhhhkrx:fix/particle-orbital-linear-order
Open

feat(particle): support continuous Birth sub-emitters#3071
hhhhkrx wants to merge 16 commits into
galacean:dev/2.0from
hhhhkrx:fix/particle-orbital-linear-order

Conversation

@hhhhkrx

@hhhhkrx hhhhkrx commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

背景

Birth 子发射器此前只在父粒子创建时固定发射一次,目标系统的发射率、距离发射、Burst 与延迟均未按父粒子独立运行。父粒子模拟又发生在 Renderer 准备渲染阶段,子发射结果会受到组件注册顺序和相机裁剪影响;同时父速度只保留了方向,无法表达绕轴、径向及其他模块共同形成的完整轨迹速度。

修改

  • 增加连续 Birth 系统模式,在父粒子整个存活期内逐父粒子运行目标 Emission,包括 Rate Over Time、Rate Over Distance、Burst、Start Delay、Loop 和 Duration。
  • 将 Emission 配置与运行游标分离,引入每个父粒子槽位和子发射槽独立持有的 EmissionRuntimeState,并在扩容、迁移、退休和清空时同步维护。
  • 复用 Transform Feedback 当前帧回读,同时为连续 Birth 与 Death 提供父粒子的当前世界位置。
  • 通过相邻采样位置差分计算父粒子完整世界速度,覆盖线性、绕轴、径向、力、重力、噪声以及发射器 Transform 位移。
  • 增加 InheritVelocityModule,保留 Initial、Current 模式以及完整 MinMaxCurve 配置;Initial 模式把父粒子轨迹速度按曲线系数叠加到子粒子初速度。
  • 增加场景级粒子系统调度器,按子发射依赖关系拓扑更新,并在相机裁剪前完成模拟和命令消费。
  • 调整绕轴、径向与线性速度的积分合成顺序,使连续子发射直接消费更新后的 TF 轨迹。

验证

  • 引擎模块构建通过。
  • engine-core 类型构建通过。
  • Particle SubEmitter、Velocity Over Lifetime、ParticleRenderer 与 Burst 定向测试通过。
  • 新增逐父粒子发射率、延迟、距离发射、Burst、槽位迁移、拓扑顺序、裁剪外更新以及完整轨迹速度测试。
  • 新增 HCFX 场景关注的绕轴 TF 位置与继承速度回归覆盖,并更新对应视觉基线。

Summary by CodeRabbit

  • New Features
    • Added particle velocity inheritance for sub-emitters (initial vs current parent velocity).
    • Introduced coordinated sub-emitter scheduling so chained emissions update in the correct order.
  • Bug Fixes
    • Improved sub-emitter Birth/Death timing, inherited properties, and transform-feedback position handling.
    • Fixed particle simulation/shader updates during instanced and partial-update passes, including preserved transform-feedback state.
  • Tests
    • Expanded sub-emitter and transform-feedback coverage, and added a WebGL2 test for velocityOverLifetime linear velocity integration.

@hhhhkrx
hhhhkrx requested a review from GuoLei1990 July 17, 2026 15:58
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Particle simulation now uses scene-level scheduling, queued sub-emitter commands, pooled emission runtime state, Birth/Death lifecycle tracking, transform-feedback synchronization, and configurable parent-velocity inheritance. Public particle exports and coverage tests are updated.

Changes

Particle system runtime

Layer / File(s) Summary
Emission sampling and velocity contracts
packages/core/src/particle/modules/*, packages/core/src/particle/enums/*, packages/core/src/particle/index.ts, packages/core/src/particle/ParticleGenerator.ts
Emission cursors use pooled runtime state, and generators expose configurable inherited velocity with deterministic random seeding.
System sub-emitter lifecycle
packages/core/src/particle/modules/SubEmitter.ts, packages/core/src/particle/modules/SubEmittersModule.ts, packages/core/src/particle/ParticleGenerator.ts
Birth and Death sub-emitters track per-particle state, sample parent motion, enqueue commands, and preserve timing and inherited spawn data.
Scene particle scheduling
packages/core/src/ComponentsManager.ts, packages/core/src/Engine.ts, packages/core/src/particle/ParticleSystemManager.ts, packages/core/src/particle/ParticleRenderer.ts
Scene managers register particle renderers and update them in dependency order while passing queued sub-emitter commands.
Feedback preservation and validation
packages/core/src/particle/ParticleTransformFeedbackSimulator.ts, tests/src/core/particle/*
Partial transform-feedback passes preserve untouched slots, with tests covering scheduling, timing, inheritance, resizing, and velocity integration.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Engine
  participant ParticleSystemManager
  participant ParentParticleRenderer
  participant ChildParticleRenderer
  participant ParticleGenerator
  Engine->>ParticleSystemManager: update(deltaTime)
  ParticleSystemManager->>ParentParticleRenderer: update parent particles
  ParentParticleRenderer->>ParticleGenerator: process births and deaths
  ParticleGenerator->>ParticleSystemManager: enqueue child emission command
  ParticleSystemManager->>ChildParticleRenderer: update with queued command
  ChildParticleRenderer->>ParticleGenerator: spawn inherited child particles
Loading

Possibly related PRs

Suggested labels: enhancement, particle

Suggested reviewers: cptbtptpbcptdtptp, guolei1990

Poem

I’m a rabbit watching particles fly,
Through queued little sparks across the sky.
Birth follows birth in ordered streams,
Velocities dance through feedback dreams.
Hop, hop—new trails now bloom! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: continuous Birth sub-emitter support in the particle system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.78775% with 97 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.58%. Comparing base (8e11477) to head (83cc51d).

Files with missing lines Patch % Lines
packages/core/src/particle/ParticleGenerator.ts 94.67% 38 Missing ⚠️
...ges/core/src/particle/modules/SubEmittersModule.ts 92.55% 28 Missing ⚠️
packages/rhi-webgl/src/GLBuffer.ts 71.11% 13 Missing ⚠️
...core/src/particle/modules/InheritVelocityModule.ts 96.00% 7 Missing ⚠️
...ckages/core/src/particle/modules/EmissionModule.ts 96.00% 6 Missing ⚠️
...ackages/core/src/particle/ParticleSystemManager.ts 97.15% 5 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #3071      +/-   ##
===========================================
+ Coverage    85.41%   85.58%   +0.16%     
===========================================
  Files          811      815       +4     
  Lines        94654    96132    +1478     
  Branches     11510    11799     +289     
===========================================
+ Hits         80849    82272    +1423     
- Misses       13715    13770      +55     
  Partials        90       90              
Flag Coverage Δ
unittests 85.58% <94.78%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 406c0ca819

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1042 to +1045
if (parentWorldVelocity && this.inheritVelocity.enabled) {
const inheritFactor = this.inheritVelocity.curve.evaluate(
normalizedEmitAge,
this.inheritVelocity._curveRand.random()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor Current inherit-velocity mode

When child.generator.inheritVelocity.mode is set to ParticleInheritVelocityMode.Current, this path still applies the sampled parent velocity only once to the child's initial direction and speed, exactly like Initial; mode is never read elsewhere. Consequently, a living child does not react when its parent velocity subsequently changes, despite the exported enum documenting that behavior for Current. Either implement the per-frame parent-velocity linkage/update or avoid exposing Current as supported.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/particle/modules/SubEmitter.ts (1)

45-66: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject the unsupported Death + System configuration.

System processing only handles Birth slots, while event dispatch excludes every System slot. The currently accepted combination therefore never emits.

  • packages/core/src/particle/modules/SubEmitter.ts#L45-L66: validate the type/mode invariant in both setters.
  • packages/core/src/particle/modules/SubEmittersModule.ts#L106-L123: reject the invalid pair during addSubEmitter.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/particle/modules/SubEmitter.ts` around lines 45 - 66,
Reject the unsupported Death + System combination. In
packages/core/src/particle/modules/SubEmitter.ts lines 45-66, enforce the
type/mode invariant in both the mode and type setters; in
packages/core/src/particle/modules/SubEmittersModule.ts lines 106-123, apply the
same validation when addSubEmitter accepts a slot so invalid configurations
cannot be registered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/particle/modules/EmissionModule.ts`:
- Around line 295-319: Update _emitByBurst to iterate through the interval from
lastPlayTime to playTime, splitting emission segments at each actual duration
boundary. Handle updates within later cycles instead of limiting the fallback to
the first cycle, and ensure ranges such as 0.1–1.1 stop at 1.0 rather than
processing the next full cycle. Reset state.currentBurstIndex only after
reaching a cycle boundary, preserving _emitBySubBurst for each segment.

In `@packages/core/src/particle/modules/SubEmittersModule.ts`:
- Around line 270-280: In the recreated-state branch of the SubEmittersModule
update, evaluate and assign startDelay before resetting state.emission. Reset
the emission to the parent’s current emission time after applying the delay,
clamping that time to target.duration for non-looping targets, instead of
resetting it to zero; preserve the existing previousParentAge and position
initialization.

In `@packages/core/src/particle/ParticleGenerator.ts`:
- Around line 1042-1069: Update the inherited-velocity handling in the particle
generation path to branch on inheritVelocity.mode: retain the existing one-time
combination for Initial mode, while Current mode must preserve the inheritance
configuration on the particle and apply the parent’s current velocity during its
lifetime. Use the existing particle update/inheritance symbols and avoid baking
Current-mode velocity into the initial direction and speed.
- Around line 709-710: Update the runtime remapping logic around the
migrateCount/runtimeMappings push to also map the wrapped front segment when
firstRetiredElement is greater than firstFreeElement. Preserve both occupied
ranges—[firstRetiredElement, end) and [0, firstFreeElement)—so front-segment
sub-emitter cursors remain mapped during shrink.

In `@packages/core/src/particle/ParticleSystemManager.ts`:
- Around line 55-59: Update ParticleSystemManager.update so commands enqueued
before the scheduler runs, including Birth sub-emitter commands from onUpdate,
remain available for consumption. Replace the unconditional
this._commands.clear() with logic that preserves the existing entry queue and
removes only commands genuinely consumed during processing, while retaining
filtering of destroyed or disabled renderers.

---

Outside diff comments:
In `@packages/core/src/particle/modules/SubEmitter.ts`:
- Around line 45-66: Reject the unsupported Death + System combination. In
packages/core/src/particle/modules/SubEmitter.ts lines 45-66, enforce the
type/mode invariant in both the mode and type setters; in
packages/core/src/particle/modules/SubEmittersModule.ts lines 106-123, apply the
same validation when addSubEmitter accepts a slot so invalid configurations
cannot be registered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1c585c35-e3a5-41c4-8466-e90ac922918d

📥 Commits

Reviewing files that changed from the base of the PR and between 9fe48c6 and 406c0ca.

⛔ Files ignored due to path filters (3)
  • e2e/fixtures/originImage/Particle_particleRenderer-sub-emitter.jpg is excluded by !**/*.jpg
  • e2e/fixtures/originImage/Particle_particleRenderer-velocity-orbital-constant.jpg is excluded by !**/*.jpg
  • packages/shader/src/Shaders/Effect/ParticleFeedback.shader is excluded by !**/*.shader
📒 Files selected for processing (17)
  • packages/core/src/ComponentsManager.ts
  • packages/core/src/Engine.ts
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/ParticleRenderer.ts
  • packages/core/src/particle/ParticleSystemManager.ts
  • packages/core/src/particle/ParticleTransformFeedbackSimulator.ts
  • packages/core/src/particle/enums/ParticleInheritVelocityMode.ts
  • packages/core/src/particle/enums/ParticleRandomSubSeeds.ts
  • packages/core/src/particle/enums/ParticleSubEmitterMode.ts
  • packages/core/src/particle/index.ts
  • packages/core/src/particle/modules/EmissionModule.ts
  • packages/core/src/particle/modules/EmissionRuntimeState.ts
  • packages/core/src/particle/modules/InheritVelocityModule.ts
  • packages/core/src/particle/modules/SubEmitter.ts
  • packages/core/src/particle/modules/SubEmittersModule.ts
  • tests/src/core/particle/SubEmitter.test.ts
  • tests/src/core/particle/VelocityOverLifetime.test.ts

Comment thread packages/core/src/particle/modules/EmissionModule.ts
Comment thread packages/core/src/particle/modules/SubEmittersModule.ts
Comment on lines +709 to +710
migrateCount > 0 &&
runtimeMappings.push({ source: firstRetiredElement, target: bufferOffset, count: migrateCount });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve the wrapped front segment when remapping runtime states.

When firstRetiredElement > firstFreeElement, the occupied ring includes both [firstRetiredElement, end) and [0, firstFreeElement). Only the tail is mapped here, so front-segment sub-emitter cursors are lost during shrink.

Proposed fix
       } else {
         migrateCount = this._currentParticleCount - firstRetiredElement;
         bufferOffset = firstFreeElement;
+        firstFreeElement > 0 &&
+          runtimeMappings.push({ source: 0, target: 0, count: firstFreeElement });
         this._firstNewElement > firstFreeElement && (this._firstNewElement -= firstFreeElement);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
migrateCount > 0 &&
runtimeMappings.push({ source: firstRetiredElement, target: bufferOffset, count: migrateCount });
} else {
migrateCount = this._currentParticleCount - firstRetiredElement;
bufferOffset = firstFreeElement;
firstFreeElement > 0 &&
runtimeMappings.push({ source: 0, target: 0, count: firstFreeElement });
this._firstNewElement > firstFreeElement && (this._firstNewElement -= firstFreeElement);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/particle/ParticleGenerator.ts` around lines 709 - 710,
Update the runtime remapping logic around the migrateCount/runtimeMappings push
to also map the wrapped front segment when firstRetiredElement is greater than
firstFreeElement. Preserve both occupied ranges—[firstRetiredElement, end) and
[0, firstFreeElement)—so front-segment sub-emitter cursors remain mapped during
shrink.

Comment thread packages/core/src/particle/ParticleGenerator.ts Outdated
Comment thread packages/core/src/particle/ParticleSystemManager.ts Outdated
GuoLei1990

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/particle/modules/SubEmittersModule.ts (1)

333-333: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize the end of a non-looping emission to 1.

At sample.time === duration, modulo returns 0, causing end-of-duration curves such as inherited velocity to evaluate at their beginning.

Proposed fix
-            const emissionNormalizedTime = duration > 0 ? (sample.time % duration) / duration : 0;
+            const emissionNormalizedTime =
+              duration > 0
+                ? target.main.isLoop
+                  ? (sample.time % duration) / duration
+                  : Math.min(sample.time / duration, 1)
+                : 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/particle/modules/SubEmittersModule.ts` at line 333, Update
the emissionNormalizedTime calculation in the SubEmittersModule sampling logic
so a non-looping emission at sample.time === duration normalizes to 1 instead of
wrapping to 0; preserve modulo-based looping behavior for times within or beyond
looping emissions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/core/src/particle/modules/SubEmittersModule.ts`:
- Line 333: Update the emissionNormalizedTime calculation in the
SubEmittersModule sampling logic so a non-looping emission at sample.time ===
duration normalizes to 1 instead of wrapping to 0; preserve modulo-based looping
behavior for times within or beyond looping emissions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9d5e48ed-ad5f-4fe5-ad4e-71f7a502df84

📥 Commits

Reviewing files that changed from the base of the PR and between d30870d and af9f5de.

⛔ Files ignored due to path filters (1)
  • e2e/fixtures/originImage/Particle_particleRenderer-velocity-orbital-constant.jpg is excluded by !**/*.jpg
📒 Files selected for processing (8)
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/ParticleRenderer.ts
  • packages/core/src/particle/ParticleSystemManager.ts
  • packages/core/src/particle/index.ts
  • packages/core/src/particle/modules/EmissionRuntimeState.ts
  • packages/core/src/particle/modules/SubEmitter.ts
  • packages/core/src/particle/modules/SubEmittersModule.ts
  • tests/src/core/particle/SubEmitter.test.ts
💤 Files with no reviewable changes (1)
  • packages/core/src/particle/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/core/src/particle/ParticleSystemManager.ts
  • packages/core/src/particle/modules/EmissionRuntimeState.ts
  • packages/core/src/particle/ParticleRenderer.ts
  • packages/core/src/particle/ParticleGenerator.ts

GuoLei1990

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
packages/core/src/particle/ParticleGenerator.ts (2)

1041-1045: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Implement Current mode instead of always baking inherited velocity at spawn.

This logic evaluates the inherit curve once and bakes the result into the particle's initial direction and speed. Current mode should not be baked here, as it is designed to continuously follow the parent's velocity during the particle's lifetime.

Gate this initialization step on this.inheritVelocity.mode === ParticleInheritVelocityMode.Initial.

🐛 Proposed fix
-    if (parentWorldVelocity && this.inheritVelocity.enabled) {
+    if (parentWorldVelocity && this.inheritVelocity.enabled && this.inheritVelocity.mode === ParticleInheritVelocityMode.Initial) {
       const inheritFactor = this.inheritVelocity.curve.evaluate(
         normalizedEmitAge,
         this.inheritVelocity._curveRand.random()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/particle/ParticleGenerator.ts` around lines 1041 - 1045,
Gate the inherited-velocity initialization block in ParticleGenerator on
this.inheritVelocity.mode === ParticleInheritVelocityMode.Initial, alongside the
existing parentWorldVelocity and enabled checks. Ensure Current mode skips
baking inherited velocity into the particle’s initial direction and speed.

692-718: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Preserve the wrapped front segment data and mappings during buffer shrink.

When firstRetiredElement > firstFreeElement, the occupied ring wraps around and includes both [firstRetiredElement, _currentParticleCount) and [0, firstFreeElement). The current logic only copies the tail segment into the new buffer. This leaves the front segment [0, firstFreeElement) filled with zeros and fails to push its runtimeMappings, causing data loss and broken sub-emitter cursors in this range.

Copy the front segment data for both the instance and feedback buffers, and include it in runtimeMappings.

🐛 Proposed fix
         } else {
           migrateCount = this._currentParticleCount - firstRetiredElement;
           bufferOffset = firstFreeElement;
+          if (firstFreeElement > 0) {
+            instanceVertices.set(new Float32Array(lastInstanceVertices.buffer, 0, firstFreeElement * floatStride), 0);
+            runtimeMappings.push({ source: 0, target: 0, count: firstFreeElement });
+          }
           this._firstNewElement > firstFreeElement && (this._firstNewElement -= firstFreeElement);
           this._firstActiveElement > firstFreeElement && (this._firstActiveElement -= firstFreeElement);
           firstRetiredElement > firstFreeElement && (this._firstRetiredElement -= firstFreeElement);
         }
 
         instanceVertices.set(
           new Float32Array(
             lastInstanceVertices.buffer,
             firstRetiredElement * floatStride * 4,
             migrateCount * floatStride
           ),
           bufferOffset * floatStride
         );
         migrateCount > 0 &&
           runtimeMappings.push({ source: firstRetiredElement, target: bufferOffset, count: migrateCount });
 
         if (useFeedback) {
+          if (firstRetiredElement > firstFreeElement && firstFreeElement > 0) {
+            this._feedbackSimulator.copyOldBufferData(0, 0, firstFreeElement * ParticleBufferUtils.feedbackVertexStride);
+          }
           this._feedbackSimulator.copyOldBufferData(
             firstRetiredElement * ParticleBufferUtils.feedbackVertexStride,
             bufferOffset * ParticleBufferUtils.feedbackVertexStride,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/particle/ParticleGenerator.ts` around lines 692 - 718,
Update the buffer-shrink migration around the existing instanceVertices.set and
_feedbackSimulator.copyOldBufferData calls to handle the wrapped case where
firstRetiredElement > firstFreeElement: after migrating the tail segment, copy
the front range [0, firstFreeElement) into the destination immediately after the
migrated tail for both instance and feedback buffers, and append a corresponding
runtimeMappings entry with the front source, destination, and count. Preserve
the current non-wrapped migration behavior.
🧹 Nitpick comments (1)
packages/core/src/particle/ParticleGenerator.ts (1)

1332-1343: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid per-frame object allocations in the hot path.

Creating new Vector3 and Color instances for every sub-emitter command generates significant garbage, especially for continuous Birth sub-emitters that run per parent particle every frame.

Consider using an object pool for ParticleSubEmitterEmissionCommand and its vector/color properties to eliminate these per-frame allocations during the render loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/particle/ParticleGenerator.ts` around lines 1332 - 1343,
Update the sub-emitter command creation near ParticleSubEmitterEmissionCommand
to reuse pooled command, Vector3, and Color instances instead of allocating them
with new for each emission. Ensure pooled objects are reset and populated with
the current values, and are returned to their pools after command processing
while preserving null behavior for absent inherited properties.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/core/src/particle/ParticleGenerator.ts`:
- Around line 1041-1045: Gate the inherited-velocity initialization block in
ParticleGenerator on this.inheritVelocity.mode ===
ParticleInheritVelocityMode.Initial, alongside the existing parentWorldVelocity
and enabled checks. Ensure Current mode skips baking inherited velocity into the
particle’s initial direction and speed.
- Around line 692-718: Update the buffer-shrink migration around the existing
instanceVertices.set and _feedbackSimulator.copyOldBufferData calls to handle
the wrapped case where firstRetiredElement > firstFreeElement: after migrating
the tail segment, copy the front range [0, firstFreeElement) into the
destination immediately after the migrated tail for both instance and feedback
buffers, and append a corresponding runtimeMappings entry with the front source,
destination, and count. Preserve the current non-wrapped migration behavior.

---

Nitpick comments:
In `@packages/core/src/particle/ParticleGenerator.ts`:
- Around line 1332-1343: Update the sub-emitter command creation near
ParticleSubEmitterEmissionCommand to reuse pooled command, Vector3, and Color
instances instead of allocating them with new for each emission. Ensure pooled
objects are reset and populated with the current values, and are returned to
their pools after command processing while preserving null behavior for absent
inherited properties.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fbce0b5c-54d3-4c1b-a0d4-89676ebb864f

📥 Commits

Reviewing files that changed from the base of the PR and between af9f5de and 36c64b6.

📒 Files selected for processing (3)
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/ParticleSystemManager.ts
  • packages/core/src/particle/modules/EmissionModule.ts
💤 Files with no reviewable changes (1)
  • packages/core/src/particle/modules/EmissionModule.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/particle/ParticleSystemManager.ts

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

完整复核 @ 0213a3c75

总结

连续 Birth 的产品语义是合理的:逐父粒子运行目标 Emission、把运行游标从配置中分离、并在相机裁剪前按依赖拓扑更新,方向与 Unity 一致。Current inherit velocity、Loop Burst、跨 Scene 校验和拓扑缓存也已在后续提交闭环。

但 Galacean 的父粒子位置由 GPU Transform Feedback 持有,当前实现为了照搬 Unity 的 CPU 逐粒子更新语义,引入了每帧同步 GPU 回读;同时 Initial 的曲线和发射入口契约仍不完整,原有 Birth Velocity flag 发生静默回归。加上此前仍未修复的 runtime-state 迁移、重建游标和稳态分配问题,当前版本仍不适合合入。

问题

  • [P1] ParticleGenerator.ts:388-401, 1502-1522 — 任意 Birth/Death slot 都把 Transform Feedback 变成每帧同步 GPU→CPU 回读

    _updateFeedback() 刚提交本帧 TF 后立即调用 _readbackFeedback(),底层 Buffer.getData() 直接执行 gl.getBufferSubData(),并要求本帧 CPU 随后消费返回的 JS 数组;因此这里形成同帧 GPU 完成等待。旧 Death 路径只在确有粒子退休时回读一次,本 PR 的 needsSubEmitterReadback 则让只有 Death slot、但本帧无人死亡的系统也每帧读完整存活区间;Birth 同帧有旧粒子和新粒子时还可能读两轮,ring wrap 后每轮再拆成两次调用。

    Unity 的 SubModule::Update 能逐父粒子访问位置,是因为 Shuriken 状态本来就在 CPU SIMD 数组中;这个成本不能直接迁移到 Galacean 的 GPU-TF 架构。至少应恢复 Death 的按需回读,并把 Birth 的“计算 emission schedule”和“获取 sample position”拆开:Rate/Burst 本帧没有 sample 时不回读,Rate Over Distance 单独走需要轨迹的位置路径。合入前请补 1k/5k 父粒子的 CPU frame time / GPU stall benchmark;若最坏场景仍需每帧同步,需明确选择异步一帧延迟、CPU 权威轨迹或 GPU 侧生成,而不是把同步点作为默认隐藏成本。

  • [P1] ParticleGenerator.ts:1030-1066, 1304-1315 / SubEmittersModule.ts:188-198Initial 的曲线时域和可用入口都不符合统一的 InheritVelocity 契约

    当前 Initial 在 spawn 时用 emissionNormalizedTime 求值一次并烘进 startSpeed + direction。这只适用于 Constant/TwoConstants;Unity 对 Curve/TwoCurves 会保存出生瞬间的 emitter velocity,并在子粒子生命周期中按 child normalized age 持续求曲线(InheritVelocityModule::Update / RequiresPerParticleVelocities)。例如 0→1 曲线在当前实现里会按目标 emission timeline 固定成一个值,出生后不再变化。

    同时 _addNewParticle() 只有收到 parentWorldVelocity 才执行 Initial:连续 Birth 会传,Death _dispatchEvent() 明确传 null,普通 _emit() 也不提供。因此同一 public module 的 Current 可用于普通系统,Initial 却只对 Birth 生效,Death 子发射静默无效。应建立一个 emission-entry-independent 的 velocity contract:普通 emission 取 Entity emitter velocity,所有 sub-emitter event 传父粒子世界速度;Initial Constant 可出生时烘焙,Initial Curve/TwoCurves 需保存 birth velocity + per-particle random,并按 child age 求值。请补普通 Initial、Death Initial、Initial Curve 和 TwoCurves 的公开行为测试。

  • [P1] ParticleGenerator.ts:1392-1418 — 连续 Birth 静默忽略现有 ParticleSubEmitterInheritProperty.Velocity

    该 public flag 仍声明“Emit the sub particle along the parent's velocity direction”。改动前 Birth 经过 _dispatchEvent(),会把父粒子出生方向作为 override;现在 Birth 绕过该路径,handler 只处理 Color/Size/Rotation,并固定传 eventWorldDirection = null。原有 Birth Velocity inherit emits along the parent's birth emission direction 回归测试也被删除,于是同一个 flag 在 Death 有效、Birth 无效且无报错。

    Unity 把速度继承统一放在 Inherit Velocity module,长期对齐可以选择废弃这个 Galacean flag;但当前 PR 不能保留公开枚举值并静默改变其中一种事件的语义。请保留旧行为,或显式移除/废弃并给出迁移与契约测试。

  • [P1] ParticleGenerator.ts:643-715 / SubEmittersModule.ts:362-375 — wrapped shrink 仍丢失前段父粒子的 Birth runtime state

    wrapped ring 的存活区间是 [firstRetiredElement, capacity) + [0, firstFreeElement),shrink 分支只生成尾段 mapping;_remapParticleRuntimeStates() 新建整表后只复制 mapping,前段仍存活父粒子的 emission cursor 变成 undefined。应让 _resizeInstanceBuffer() 生成唯一的 segment plan,并由同一份 plan 驱动 CPU instance、feedback 与 runtime-state 三类迁移,而不是三处分别维护区间。请补制造 wrap 后收缩 maxParticles、断言两个 segment 都延续原 cursor 的公开回归测试。

  • [P1] SubEmittersModule.ts:260-270 / EmissionModule.ts:206-225 — 为存活父粒子重建 slot state 时从 0 补发全部历史 Rate Over Time

    重建分支把 previousParentAge 设为当前父龄,却执行 emission.reset(seed, 0)。下一帧 _emitByRateOverTime()playTime - frameRateTime 推进,因而从 0 追赶到当前父龄;父粒子已活 0.5s、rate=10 时会突然补出约 5 个历史粒子。请先采样 start delay,再把 cursor 设为当前 delay-adjusted emission time(non-loop clamp 到 duration),并补动态给老父粒子添加 Birth slot 的反向测试。

  • [P1] SubEmittersModule.ts:203-233,352-374 / ParticleGenerator.ts:1332-1343 / ParticleSystemManager.ts:60-87 — 连续 Birth 的稳态生命周期仍持续创建并丢弃对象

    每个父粒子 Birth × slot 都创建 BirthSubEmitterRuntimeStateEmissionRuntimeState、Rand/Vector/sample pool;退休时整个 ring slot 置 null。每个 emission sample 又创建 command 和最多六个 Vector/Color,manager 清 Map 后对应数组也失去复用。高 rate 配置会使分配量随父粒子出生量和子 sample 数线性增长。

    runtime state 应由 ring slot 长期持有,退休只 reset;command buffer 应由 manager 持有并复用,generator 只写入可复用 entry。请用稳定拓扑持续发射后的对象身份或 allocation counter 测试,证明 warm-up 后不再随 sample 数持续分配。

简化建议

  • EmissionRuntimeState 是内部 cursor owner,不应从 public particle barrel 导出;当前测试应改走 generator/update 的公开行为,而不是为测试扩大 API。
  • 删除无 manager 时的同步 _emitFromSubEmitter() fallback,让 ParticleSystemManager 成为唯一调度 owner;测试应通过 scene/engine update。
  • 父速度目前用相邻帧位置差得到整帧平均弦速度;Unity 使用当前 velocity + animatedVelocity。若保留差分,应明确它是近似,并增加不同 timestep 下轨道/加速轨迹的稳定性测试。

已关闭 / 不重复

Current inherit velocity、Loop Burst、脚本 emit() 命令丢失、跨 Scene 配置与迁移、拓扑图每帧重建,以及 orbital 积分说明已在后续提交闭环,本轮不重复提出。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

结论

本轮完整复核了 0213a3c75cd431ac3e39fcf1e6c9063a68e2e198...db59b0830695aa0cd57ef66612d351b4e4fc3a64 的单 commit 增量,并回看完整 PR 数据流、历史 finding 与测试。最新 commit 正确缩小了同步回读范围,也把 Entity 速度与父粒子速度拆到不同配置 owner;但核心链路仍有多项 P1,且新拆分引入了低频采样下的轨迹速度失真,当前不适合合入。

阻塞级别:P1。实际 review 动作:REQUEST_CHANGES。目标 HEAD:db59b0830695aa0cd57ef66612d351b4e4fc3a64。当前新 HEAD 只有 labeler check,完整 build/test CI 尚未挂载,PR 也处于 DIRTY;冲突解决后需按新 HEAD 重新复核。自动 CR 不替代人工 Reviewer 的合入门禁,最终仍需人工审核确认。

已关闭问题清单

  • Current 模式未实现:已由 7dd24d0ac 的 TF/shader 路径和公开行为测试修复。
  • Loop Burst 跨周期、脚本 emit() 命令丢失:分别由 be5bc7bb8af9f5de21 闭环。
  • 跨 Scene 配置与迁移崩溃:1fc541fbd 在配置边界拒绝,a7f732e28 删除热路径 throw、迁移后优雅跳过。
  • 每帧重建依赖拓扑:0213a3c75 改为拓扑缓存、入口标脏和 scratch 复用。
  • orbital 积分行为未说明:PR 描述已明确记录 orbit/radial/linear 合成顺序变化。
  • 任意 Birth/Death slot 空闲帧都回读:db59b0830 已让 Death 仅在退休时回读,并让无 Rate/Burst 样本的 Birth 帧跳过回读;剩余 Rate Over Distance、同步 stall 与稀疏速度问题见下文。
  • 普通系统的 Initial Constant 入口缺失:db59b0830 已让 InheritVelocityModule 捕获 Entity 位移速度并覆盖普通 emission;Curve/TwoCurves 的生命周期契约仍未闭环。

问题

  • [P1] ParticleGenerator.ts:392-400,1361-1451 / EmissionModule.ts:175-206 / SubEmittersModule.ts:245-405 — 选择性回读仍保留同步 stall,并把父速度降成“上次有样本以来的平均弦速度”

    _prepareEmissionSamples() 只要 rateOverDistance > 0 就返回 true,因此即使父粒子静止、该帧没有 distance sample,仍会对完整存活区间执行同步 gl.getBufferSubData();高频 Rate/Burst 也会在每个有样本帧同步等待 GPU。更严重的是,无样本帧不再调用 _processBirthRange(),所以 previousSampleParentAgepreviousWorldPosition 不更新;下一次样本的速度变为跨 N 帧的 (P_now-P_lastSample)/Δt。例如父粒子一秒绕完整圈、Birth rate=1/s 时,弦位移接近 0,子粒子会继承近 0 速度,而发射瞬间切向速度非 0。现有 orbital 测试只覆盖 0.1s 首帧,无法证伪该回归。

    应保留 TF/GPU 轨迹为唯一权威 owner,明确选择可验证的速度/采样协议:由 GPU 产出完整轨迹速度或发射结果,或采用明确的一帧异步协议;不要再新增 CPU 镜像状态。至少补低频 orbital/加速轨迹的 timestep 稳定性测试,以及 1k/5k 父粒子的 CPU frame time 与 GPU stall benchmark。仅统计 _readbackFeedback 调用次数不能证明热路径成本可接受。

  • [P1] InheritVelocityModule.ts:99-114 / SubEmitter.ts:15-26 / SubEmittersModule.ts:166-208 / ParticleGenerator.ts:1272-1319,1417-1445 — 速度契约仍分裂成三份互不完整的 public truth

    最新 commit 正确把 Entity 速度归还给 InheritVelocityModule,把父粒子速度缩放移到 SubEmitter.inheritVelocity;但三条路径仍互相矛盾:① Initial Curve/TwoCurves 仍在出生时按 system normalizedEmitAge 求值一次并烘进初速度,Unity 的 RequiresPerParticleVelocities() 则保存出生时 emitter velocity、按 child normalized age 持续求曲线;② 新的 slot curve 文档对 Birth/Death 都成立,但 Death 明明已从 TF 读到 velocity,却向命令传 parentWorldVelocity=null,因此该字段对 Death 静默无效;③ 旧 ParticleSubEmitterInheritProperty.Velocity 仍公开存在,只在 Death 覆盖方向,Birth 继续静默忽略,原 Birth 契约测试被替换成了新字段测试。

    应保留两个明确 owner:InheritVelocityModule 只拥有 Entity velocity,并为 Initial Curve/TwoCurves保存 birth velocity + per-particle random、按 child age 求值;SubEmitter.inheritVelocity 只拥有父粒子完整速度缩放,并让 Birth/Death 都传入同一 TF 速度协议。随后删除或正式废弃 ParticleSubEmitterInheritProperty.Velocity,给出 inheritVelocity.constant=1 的迁移并按新公开契约重写旧测试;不要用 compatibility branch 继续维护第四条路径。

  • [P1] ParticleGenerator.ts:639-711 / SubEmittersModule.ts:419-433 — wrapped shrink 仍只迁移尾段,前段粒子、feedback 与 Birth cursor 一起丢失

    环绕存活区间是 [firstRetiredElement, capacity) + [0, firstFreeElement);当前 shrink 的 firstRetiredElement > firstFreeElement 分支仍只 copy/map 第一段。runtimeMappings、CPU instance copy 和 feedback copy 同时遗漏前段,存活父粒子的状态会消失或与粒子数据错配。

    应让 _resizeInstanceBuffer() 生成一份覆盖两个 segment 的权威迁移计划,再机械驱动 instance、feedback 和 runtime-state 三类 copy,删除三处独立区间 ownership。请补 wrap 后收缩 maxParticles、断言两个 segment 均保留粒子与 emission cursor 的公开回归测试。

  • [P1] SubEmittersModule.ts:261-305 / EmissionModule.ts:245-264 — 动态重建 slot state 仍把 emission cursor reset 到 0,下一帧补发全部历史 Rate Over Time

    重建分支把 previousEmissionParentAge 设为当前父龄,却仍执行 state.emission.reset(seed, 0)。下一帧 _emitByRateOverTime() 以当前 emission time 减 0 计算 backlog;父粒子已活 0.5s、rate=10 时会单帧补出约 5 个不存在的历史样本。新两阶段 prepare/complete 只把双时钟显式化,没有修复权威 cursor。

    应让 EmissionRuntimeState.frameRateTime 成为唯一发射游标 owner:采样 start delay 后,把重建 cursor 设为当前 delay-adjusted emission time,non-loop 时 clamp 到 duration。补“给存活父粒子动态添加 Birth slot,下一帧不补历史”的反向公开测试。

  • [P1] SubEmittersModule.ts:212-242,410-416 / ParticleGenerator.ts:1323-1359 / ParticleSystemManager.ts:62-89 — 稳态仍按父粒子和 emission sample 线性制造 GC

    每次父粒子 Birth 都新建 BirthSubEmitterRuntimeState、内部 EmissionRuntimeState/Rand/Vector,退休又把 ring slot 置 null;每个 sample 仍新建 command 和最多六个 Vector/Color,manager clear() 后连 per-target array 一起失去复用。最新 commit 还为每个 slot 增加独立 Rand,但没有改变热路径对象生命周期。

    应让 ring slot 长期持有并 reset runtime state,让 ParticleSystemManager 长期持有可复用 command buffer/entry,generator 只写值;删除 retire -> null -> new 与逐 sample new 两条路径。请用对象身份、allocation counter 或真实 allocation benchmark证明 warm-up 后分配不再随 sample 数增长。

  • [P2] particle/index.ts:17 / EmissionRuntimeState.ts:12 / RateOverTimeReplay.test.ts — 内部 cursor 仍为测试扩大公共 API

    EmissionRuntimeState 的生产 owner 只有 emission/sub-emitter 内部路径,却继续从 public barrel 导出,测试再直接调用 _getEmissionSamples()。应删除 public export,把有效 burst 契约改由 generator/update 公开链路验证;不要保留 public wrapper 或第二采样入口喂旧测试。

  • [P2] ParticleGenerator.ts:1352-1358 — 无 manager 的同步直发仍是第二条调度状态机

    支持路径中的 Birth/Death 都由已注册 renderer 的 scene manager update 产生;同步 fallback 绕过拓扑顺序、命令 owner 和目标 update clock,只服务直接驱动内部 _update 的非公开用法。应保留 ParticleSystemManager 为唯一调度 owner并删除 fallback,相关测试迁移到 scene/engine update。

  • [P2] SubEmittersModule.ts:101-132 / InheritVelocityModule.ts:11 — 新公开 surface 的 TSDoc 未随契约更新完整

    addSubEmitter() 现在返回 SubEmitter,但公开 TSDoc 缺少 @returns;本轮修改的 public InheritVelocityModule 类仍使用单行块。请按同包公开模块惯例补完整多行 TSDoc,并让返回值说明 slot 是后续配置 inheritVelocity 的入口。

架构、熵增与测试治理

沿链路上游一层,ParticleSystemManager 应唯一拥有拓扑更新与命令消费,EmissionRuntimeState 应唯一拥有 Rate/Burst/Distance cursor,InheritVelocityModule 应唯一拥有 Entity velocity,SubEmitter 应唯一拥有父粒子 velocity scale;下游一层,TF buffer 是父轨迹事实,_processBirthRange() 只应把该事实和 emission plan 组装成命令,目标 generator 只负责 spawn。

本轮把错误复用 target InheritVelocityModule 的父速度配置移回 slot,owner 方向是净减法;但系统实际从“一套 emission + 一个父采样时钟”增加为 prepare/complete 两阶段、两个 parent-age 时钟、prepared interval/flag、缓存 rate 以及第三个 velocity public control。新增状态没有替代旧 Velocity bit、同步 fallback、public runtime cursor 或逐 sample command ownership,复杂度仍向上下游扩散。应按上述 owner 删除旧路径,而不是再加同步层。

测试治理方面,新 readback spy 只锁定私有调用次数,没有覆盖 stall、Rate Over Distance 空转或低频曲线轨迹;wrapped shrink、动态重建 backlog、稳态 allocation、Death slot velocity、Initial Curve/TwoCurves 均无反向证伪。旧 Birth Velocity flag 测试被新 slot 测试替换,却未删除/迁移旧公开契约;RateOverTimeReplay 又通过 public export 锁定内部 cursor。权威契约切换后应删除失效 fixture/私有测试,按新公开链路重写,绝不能为旧测试保留 compatibility branch、fallback、wrapper、镜像状态或第二条转换/校验路径。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

结论

本轮完整复核了 db59b0830695aa0cd57ef66612d351b4e4fc3a64...9f0271349b84e7140dafb87f0be84df1927a9911 的单 commit 增量,并回看完整 PR diff、未关闭 review thread、Unity Inherit Velocity 参考实现以及 TF 上下游协议。最新 commit 正确把父粒子本帧世界轨迹速度交给 GPU Transform Feedback 产出,关闭了稀疏采样跨帧弦速度失真,也恢复了 Birth Velocity 方向继承;但同步回读、Death 速度协议、Initial 曲线生命周期、环形迁移、动态游标和稳态分配仍有多项 P1,当前不适合合入。

阻塞级别:P1。实际 review 动作:REQUEST_CHANGES。目标 HEAD:9f0271349b84e7140dafb87f0be84df1927a9911。当前 PR 仍为 DIRTY,该 HEAD 仅有 labeler check,完整 build/test CI 尚未挂载;冲突解决后需按新 HEAD 重新复核。自动 CR 不替代人工 Reviewer 的合入门禁,最终仍需人工审核确认。

已关闭问题清单

  • Current 模式未实现:7dd24d0ac 已通过 TF/shader 路径和公开行为测试闭环。
  • Loop Burst 跨周期、脚本 emit() 命令丢失:分别由 be5bc7bb8af9f5de21 修复。
  • 跨 Scene 配置与迁移崩溃:1fc541fbd 在配置边界拒绝,a7f732e28 删除热路径 throw、迁移后优雅跳过。
  • 每帧重建依赖拓扑:0213a3c75 改为拓扑缓存、入口标脏和 scratch 复用。
  • orbital 积分行为未说明:PR 描述已明确记录 orbit/radial/linear 合成顺序变化。
  • 任意 Birth/Death slot 空闲帧都回读、普通系统 Initial Constant 入口缺失:db59b0830 已分别缩小回读触发范围并补齐 Entity velocity 入口。
  • 稀疏 Birth 把父速度降为跨 N 帧平均弦速度:9f0271349 新增 TF world position/trajectory velocity,按当前 TF 帧产出速度,并补 0.1s/0.05s orbital 与 Entity motion 回归测试。
  • Birth 静默忽略 ParticleSubEmitterInheritProperty.Velocity9f0271349 已恢复为使用父粒子轨迹方向,且测试确认不额外继承速度大小。

问题

  • [P1] ParticleGenerator.ts:392-400,1361-1452,1540-1560 / EmissionModule.ts:175-205 / ParticleBufferUtils.ts:19-25 — 选择性回读仍是同步 GPU stall,最新协议还把所有 TF 系统的反馈载荷从 24B 扩到 48B

    _prepareEmissionSamples() 只要 rateOverDistance > 0 就要求位置,因此静止父粒子也会每帧对完整存活区间执行 Buffer.getData();高频 Rate/Burst 同样会在每个有样本帧回读。底层就是同帧 gl.getBufferSubData(),CPU 必须等待刚提交的 TF 完成。最新 commit 修复了速度事实,却让每个 TF slot 从两个 vec3 变为四个 vec3;这不仅使回读字节翻倍,也让普通 Noise/VOL/Current-inherit 系统承担额外 TF 写带宽,即使它们没有 Birth slot。

    应保留 TF/GPU 轨迹为唯一权威 owner,但需要选择可验证的消费协议:由 GPU 直接产出 emission 结果,或采用明确的一帧异步 readback;不要重新增加 CPU 位置/速度镜像。若继续当前同步协议,至少提供 1k/5k 父粒子的 CPU frame time、GPU stall 与 TF bandwidth benchmark,并证明 Rate Over Distance 静止场景和高频样本场景可接受;仅统计 _readbackFeedback 次数不能证明热路径成本。

  • [P1] ParticleGenerator.ts:1563-1615 / SubEmittersModule.ts:163-205 / SubEmitter.ts:24-26 — Death 仍绕过新的权威轨迹通道,导致方向缺模块速度且 slot inheritVelocity 永远无效

    最新 TF 已在 offset 6..11 产出权威 worldPosition + trajectoryVelocity,Birth 也直接消费它;Death 却继续读取 offset 0..5 的 simulation position / localVelocity,再用当前或出生 rotation 手工转世界。localVelocity 不含以位置积分的 orbital/radial 速度,也不含 Local simulation 下 Entity Transform 的移动;因此同一父粒子的 Birth Velocity 会沿完整轨迹,Death Velocity 仍沿不完整基础速度。随后 _dispatchEvent() 又固定把 parentWorldVelocitynull,所以公开的 SubEmitter.inheritVelocity 对 Death slot 静默 no-op,和“when a sub particle is emitted”的 TSDoc 冲突。

    应保留 TF v_FeedbackWorldPosition/v_FeedbackTrajectoryVelocity 为父轨迹唯一 owner:Death 直接读取 offset 6..11,把同一 trajectory velocity 同时提供给方向投影和 SubEmitter.inheritVelocity,并删除 offset 0..5 → simulation-space/spawn-rotation 的平行转换路径。补 Death orbital、Entity motion、slot curve 的公开行为测试。

  • [P1] InheritVelocityModule.ts:99-114 / ParticleGenerator.ts:1027-1061Initial Curve/TwoCurves 仍在出生时按 emission timeline 烘焙一次,未按 child lifetime 求值

    _getInitialVelocity(normalizedEmitAge) 对所有曲线模式都在 spawn 时求值一次并改写 direction/startSpeed。Unity 只对 Constant/TwoConstants 走 ApplyOnBirth();Initial Curve/TwoCurves 会保存出生时 emitter velocity,再由 UpdateTpl 按每个 child 的 NormalizedTime 持续求曲线。当前 0→1 曲线会被固定在 system/sub-emitter emission time 的一个值,子粒子出生后不再变化,普通 emission 与 sub-emitter 都受影响。

    应保留 InheritVelocityModule 为 Entity velocity 唯一 owner:Constant/TwoConstants 可继续出生时烘焙;Curve/TwoCurves 保存 birth emitter velocity 与 per-particle random,并在 child age 更新路径求值。按公开 update 链路补 Initial Curve/TwoCurves 反向测试,不要增加只服务旧测试的 wrapper 或第二套求值入口。

  • [P1] ParticleGenerator.ts:639-711 / SubEmittersModule.ts:425-438 — wrapped shrink 仍只迁移尾段,前段 instance、feedback 与 emission cursor 一起丢失

    环绕存活区间是 [firstRetiredElement, capacity) + [0, firstFreeElement);shrink 的 firstRetiredElement > firstFreeElement 分支仍只 copy/map 第一段。新 48B feedback、CPU instance 和 Birth runtime state 都机械继承了这份不完整区间判断,前段存活粒子会消失或与 cursor 错配。

    应让 _resizeInstanceBuffer() 生成一份覆盖两个 segment 的权威迁移计划,再由该计划机械驱动 instance、feedback 和 runtime-state copy,删除三处独立区间 ownership。补 wrap 后收缩 maxParticles、断言两段粒子与 emission cursor 都保留的公开回归测试。

  • [P1] SubEmittersModule.ts:257-268 / EmissionModule.ts:245-264 — 动态重建 slot state 仍把权威 emission cursor reset 到 0,下一帧补发全部历史 Rate Over Time

    给存活父粒子新增/替换 Birth slot 时,代码把 previousEmissionParentAge 设为当前父龄,却执行 state.emission.reset(seed, 0)。下一帧 _emitByRateOverTime() 用当前 delay-adjusted emission time 减 0,父粒子已活 0.5s、rate=10 时会单帧补出约 5 个不存在的历史样本;最新 commit 删除采样位置镜像并未改变该游标。

    应保留 EmissionRuntimeState.frameRateTime 为唯一 Rate 游标 owner:先采样 start delay,再把重建 cursor 设为当前 delay-adjusted emission time,non-loop 时 clamp 到 duration。补“给存活父粒子动态添加 Birth slot,下一帧不补历史”的公开反向测试。

  • [P1] SubEmittersModule.ts:209-238,415-423 / ParticleGenerator.ts:1323-1359 / ParticleSystemManager.ts:62-90 — 稳态仍按父粒子和 emission sample 线性制造 GC

    最新 commit 从每个 Birth state 删除了一个 Vector3 和两个采样时钟,方向是净减法;但每次父粒子 Birth × slot 仍 new BirthSubEmitterRuntimeState、内部 EmissionRuntimeState/Rand/Vector,且还即时 new Rand 采样 Start Delay,退休后整个 ring slot 置 null。每个 sample 又新建 command 和最多六个 Vector/Color,manager clear() 后 per-target array 也失去复用。分配量仍随父粒子出生量和子 sample 数线性增长。

    应让 ring slot 长期持有并 reset runtime state,让 ParticleSystemManager 长期持有可复用 target buffer/command entry,generator 只写值;删除 retire -> null -> new、逐 slot new Rand 与逐 sample new command 三条稳态路径。用对象身份、allocation counter 或真实 allocation benchmark证明 warm-up 后分配不再随 sample 数增长。

  • [P2] particle/index.ts:17 / EmissionRuntimeState.ts:12 / RateOverTimeReplay.test.ts:149-164 — 内部 cursor 仍为测试扩大公共 API

    EmissionRuntimeState 的生产 owner 只有 emission/sub-emitter 内部链路,却继续从 public barrel 导出,测试再直接调用 _getEmissionSamples()。应删除 public export,把 Burst 边界契约改由 generator/update 的公开行为验证;不要保留 public wrapper、compat export 或第二采样入口喂旧测试。

  • [P2] ParticleGenerator.ts:1352-1358 — 无 manager 的同步直发仍是第二条调度状态机

    支持路径中的 Birth/Death 都由已注册 renderer 的 scene manager update 产生;同步 fallback 绕过拓扑顺序、命令 owner 和目标 update clock,只服务直接驱动内部 _update 或未入 Scene 的非公开用法。应保留 ParticleSystemManager 为唯一命令调度 owner并删除 fallback,相关测试迁移到 scene/engine update。

  • [P2] SubEmittersModule.ts:98-130 / InheritVelocityModule.ts:11 — 新公开 surface 的 TSDoc 仍未完整表达返回值与类契约

    addSubEmitter() 返回可继续配置 inheritVelocitySubEmitter,但公开 TSDoc 缺少 @returns;public InheritVelocityModule 类仍使用单行块,偏离同包公开模块的多行 TSDoc 约定。请补完整多行 TSDoc,并明确返回 slot 是后续配置 per-slot velocity scale 的入口。

架构、熵增与测试治理

沿直接链路上游一层,ParticleSystemManager 应唯一拥有拓扑更新与命令消费,EmissionRuntimeState 应唯一拥有 Rate/Burst/Distance cursor,InheritVelocityModule 应唯一拥有 Entity velocity;下游一层,TF buffer 应唯一拥有父粒子的 world position/trajectory velocity,SubEmittersModule 只把 emission plan 与该事实组装为命令,SubEmitter.inheritVelocity 只配置父轨迹速度缩放,目标 generator 只负责 spawn。

本轮从 CPU Birth state 删除 previousWorldPosition/previousSampleParentAge,改由 TF 直接产出本帧轨迹事实,并恢复 direction-only Velocity 消费,owner 方向是净减法;但同时把所有 TF slot 从 24B 扩到 48B,而 Death 仍维护 offset 0..5 + rotation 的旧转换并把新 velocity 丢弃,形成同一轨迹事实的平行协议。应保留 offset 6..11 的新权威协议,删除 Death 旧转换;不要再加同步层或 CPU 镜像。

测试治理方面,新 sparse orbital / Entity motion / Birth direction 测试能反向证伪本轮修复,但 readback spy 仍只锁定私有调用次数,不能证明 stall 与 bandwidth 可接受。Death trajectory/slot curve、Initial Curve/TwoCurves、wrapped shrink、动态重建 backlog、稳态 allocation 均无公开反向测试;RateOverTimeReplay 仍通过 public export 锁定内部 cursor。权威契约切换后应删除失效 fixture/私有测试并按公开链路重写,绝不能为了旧测试保留 compatibility branch、legacy fallback、wrapper、镜像状态或第二条转换/校验路径。

@hhhhkrx
hhhhkrx requested a review from GuoLei1990 July 25, 2026 10:49

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

结论

本轮完整复核了 9f0271349b84e7140dafb87f0be84df1927a9911...3e5da780893278834e5262874d32db4bb0cd03d7 的增量,并以第二父提交 8e114779815954d3a62c7e8dd5758d0e076b3111...3e5da780893278834e5262874d32db4bb0cd03d7 回看合并后的实际 PR tree、未关闭 thread、Unity Inherit Velocity 参考实现以及 TF 上下游协议。本轮只有一次 dev/2.0 merge:冲突已解决,clone/#3060 上游改动已正确纳入,完整 CI 全绿;但没有 PR-owned 修复 commit,上一轮仍开放的同步回读、Death/Initial 速度协议、环形迁移、动态游标和稳态分配问题均继续成立,当前不适合合入。

阻塞级别:P1。实际 review 动作:REQUEST_CHANGES。目标 HEAD:3e5da780893278834e5262874d32db4bb0cd03d7。自动 CR 不替代人工 Reviewer 的合入门禁,最终仍需人工审核确认。

已关闭问题清单

  • Current 模式未实现:7dd24d0ac 已通过 TF/shader 路径和公开行为测试闭环。
  • Loop Burst 跨周期、脚本 emit() 命令丢失:分别由 be5bc7bb8af9f5de21 修复。
  • 跨 Scene 配置与迁移崩溃:1fc541fbd 在配置边界拒绝,a7f732e28 删除热路径 throw、迁移后优雅跳过。
  • 每帧重建依赖拓扑:0213a3c75 改为拓扑缓存、入口标脏和 scratch 复用。
  • orbital 积分行为未说明:PR 描述已明确记录 orbit/radial/linear 合成顺序变化。
  • 任意 Birth/Death slot 空闲帧都回读、普通系统 Initial Constant 入口缺失:db59b0830 已分别缩小回读触发范围并补齐 Entity velocity 入口。
  • 稀疏 Birth 跨帧弦速度失真、Birth 静默忽略 ParticleSubEmitterInheritProperty.Velocity9f0271349 已改由 TF 产出本帧轨迹速度并恢复 direction-only 继承,相关回归测试有效。
  • ParticleGenerator.inheritVelocity 字段缺少 TSDoc:3e5da7808 合并新版 clone 基础设施时已补齐字段说明;剩余 public surface 问题见下文。

问题

  • [P1] ParticleGenerator.ts:383-390,1358-1400,1537-1557 / EmissionModule.ts:171-202 / ParticleBufferUtils.ts:19-25 — 选择性回读仍是同步 GPU stall,且所有 TF 系统继续承担 48B feedback 协议

    _prepareEmissionSamples() 只要 rateOverDistance > 0 就要求位置,因此静止父粒子也会每帧对完整存活区间执行 Buffer.getData();高频 Rate/Burst 同样在有样本帧同步回读。底层 GLBuffer.getData() 直接调用同帧 gl.getBufferSubData(),CPU 必须等待刚提交的 TF。为修复轨迹事实,feedback 又从两个 vec3 扩为四个 vec3,普通 Noise/VOL/Current-inherit 系统即使没有 Birth slot,也要承担额外 TF 写带宽。

    应保留 TF/GPU 轨迹为唯一权威 owner,但改为 GPU 直接产出 emission 结果或明确的一帧异步 readback;不要增加 CPU 位置/速度镜像。若坚持同步协议,至少提供 1k/5k 父粒子的 CPU frame time、GPU stall 与 TF bandwidth benchmark,并覆盖 Rate Over Distance 静止场景和高频样本场景;私有方法调用次数不能证明热路径成本可接受。

  • [P1] ParticleGenerator.ts:1560-1612 / SubEmittersModule.ts:163-205 / SubEmitter.ts:25-26 — Death 仍绕过权威轨迹通道,方向缺模块速度且 slot inheritVelocity 永远无效

    TF offset 6..11 已产出权威 worldPosition + trajectoryVelocity,Birth 直接消费;Death 仍读取 offset 0..5 的 simulation position / localVelocity,再用当前或出生 rotation 手工转世界。该 localVelocity 不含 orbital/radial 位置积分速度,也不含 Local simulation 下 Entity Transform 移动。随后 _dispatchEvent() 又固定传 parentWorldVelocity=null,使公开的 SubEmitter.inheritVelocity 对 Death slot 静默 no-op。

    应保留 TF v_FeedbackWorldPosition/v_FeedbackTrajectoryVelocity 为父轨迹唯一 owner:Death 直接读取 offset 6..11,把同一 trajectory velocity 同时交给方向投影和 SubEmitter.inheritVelocity,并删除 offset 0..5 → simulation-space/spawn-rotation 的平行转换。补 Death orbital、Entity motion 与 slot curve 的公开行为测试。

  • [P1] InheritVelocityModule.ts:98-113 / ParticleGenerator.ts:1019-1053Initial Curve/TwoCurves 仍在出生时按 emission timeline 烘焙一次

    _getInitialVelocity(normalizedEmitAge) 对所有曲线模式都在 spawn 时求值并改写 direction/startSpeed。Unity 只让 Constant/TwoConstants 走 ApplyOnBirth();Initial Curve/TwoCurves 保存出生时 emitter velocity,并在 child lifetime 按 particle normalized age 持续求曲线。当前 0→1 曲线会固定在 system/sub-emitter emission time 的单个值,子粒子出生后不再变化。

    应保留 InheritVelocityModule 为 Entity velocity owner:Constant/TwoConstants 可出生时烘焙;Curve/TwoCurves 保存 birth emitter velocity 与 per-particle random,在 child update 路径求值。按公开 update 链路补 Initial Curve/TwoCurves 反向测试,不要增加只服务旧测试的 wrapper 或第二套求值入口。

  • [P1] ParticleGenerator.ts:603-702 / SubEmittersModule.ts:425-438 — wrapped shrink 仍只迁移尾段,前段 instance、feedback 与 emission cursor 一起丢失

    环绕存活区间是 [firstRetiredElement, capacity) + [0, firstFreeElement);shrink 的 firstRetiredElement > firstFreeElement 分支只 copy/map 第一段。CPU instance、48B feedback 和 Birth runtime state 都机械继承这份不完整区间判断,前段存活粒子会消失或与 cursor 错配。

    应让 _resizeInstanceBuffer() 生成一份覆盖两个 segment 的权威迁移计划,再机械驱动 instance、feedback 和 runtime-state copy,删除三处独立区间 ownership。补 wrap 后收缩 maxParticles、断言两段粒子与 emission cursor 都保留的公开回归测试。

  • [P1] SubEmittersModule.ts:257-268 / EmissionModule.ts:245-264 — 动态重建 slot state 仍把权威 Rate cursor reset 到 0

    给存活父粒子新增/替换 Birth slot 时,代码把 previousEmissionParentAge 设为当前父龄,却执行 state.emission.reset(seed, 0)。下一帧 _emitByRateOverTime() 用当前 delay-adjusted emission time 减 0,父粒子已活 0.5s、rate=10 时会单帧补发约 5 个不存在的历史样本。

    应保留 EmissionRuntimeState.frameRateTime 为唯一 Rate 游标 owner:采样 start delay 后,把重建 cursor 设为当前 delay-adjusted emission time,non-loop 时 clamp 到 duration。补“给存活父粒子动态添加 Birth slot,下一帧不补历史”的公开反向测试。

  • [P1] SubEmittersModule.ts:209-238,415-423 / ParticleGenerator.ts:1320-1355 / ParticleSystemManager.ts:62-90 — 稳态仍按父粒子和 emission sample 线性制造 GC

    每次父粒子 Birth × slot 都新建 BirthSubEmitterRuntimeState、内部 EmissionRuntimeState/Rand/Vector,退休后整个 ring slot 置 null;每个 sample 又新建 command 和最多六个 Vector/Color,manager clear() 后 per-target array 也失去复用。分配量仍随父粒子出生量和子 sample 数线性增长。

    应让 ring slot 长期持有并 reset runtime state,让 ParticleSystemManager 长期持有可复用 target buffer/command entry,generator 只写值;删除 retire -> null -> new、逐 slot new Rand 与逐 sample new command 三条稳态路径。用对象身份、allocation counter 或真实 allocation benchmark证明 warm-up 后分配不再随 sample 数增长。

  • [P2] particle/index.ts:17 / EmissionRuntimeState.ts / RateOverTimeReplay.test.ts:149-164 — 内部 cursor 仍为测试扩大公共 API

    EmissionRuntimeState 的生产 owner 只有 emission/sub-emitter 内部链路,却继续从 public barrel 导出,测试再直接调用 _getEmissionSamples()。应删除 public export,把 Burst 边界契约改由 generator/update 的公开行为验证;不要保留 public wrapper、compat export 或第二采样入口喂旧测试。

  • [P2] ParticleGenerator.ts:1349-1355 — 无 manager 的同步直发仍是第二条调度状态机

    支持路径中的 Birth/Death 都由已注册 renderer 的 scene manager update 产生;同步 fallback 绕过拓扑顺序、命令 owner 和目标 update clock,只服务直接驱动内部 _update 或未入 Scene 的非公开用法。应保留 ParticleSystemManager 为唯一命令调度 owner并删除 fallback,相关测试迁移到 scene/engine update。

  • [P2] SubEmittersModule.ts:98-130 / InheritVelocityModule.ts:11 — 剩余 public surface 的 TSDoc 仍不完整

    addSubEmitter() 返回可继续配置 inheritVelocitySubEmitter,但缺少 @returns;public InheritVelocityModule 类仍使用单行块,偏离同包公开模块的多行 TSDoc 约定。请补完整多行 TSDoc,并明确返回 slot 是后续配置 per-slot velocity scale 的入口。

架构、熵增与测试治理

上游一层,ParticleSystemManager 应唯一拥有拓扑更新与命令消费,EmissionRuntimeState 应唯一拥有 Rate/Burst/Distance cursor,InheritVelocityModule 应唯一拥有 Entity velocity;下游一层,TF buffer 应唯一拥有父粒子的 world position/trajectory velocity,SubEmittersModule 只把 emission plan 与该事实组装为命令,SubEmitter.inheritVelocity 只配置父轨迹速度缩放,目标 generator 只负责 spawn。

本轮 merge 把新版 clone/DataObject 协议与 #3060 合入,目标分支的 clone hook 和现有 clone 回归测试能覆盖 SubEmitter 配置复制,没有新增 PR-local owner;但它也没有减少本 PR 已有的熵:Death 仍维护 offset 0..5 + rotation 的平行轨迹转换,manager fallback 仍是第二调度路径,public runtime cursor 仍把内部状态暴露给测试,逐粒子 runtime state 与逐 sample command 仍是两套一次性对象生命周期。应保留上述权威 owner,删除这些旧路径,而不是再加同步层、镜像状态或 compatibility branch。

测试方面,完整 CI 已全绿,sparse orbital / Entity motion / Birth direction 与 clone 测试能反向证伪各自修复;但 readback spy 只锁定私有调用次数,Death trajectory/slot curve、Initial Curve/TwoCurves、wrapped shrink、动态重建 backlog、稳态 allocation 均无公开反向测试,RateOverTimeReplay 仍通过 public export 锁定内部 cursor。权威契约切换后应删除失效 fixture/私有测试并按公开链路重写,绝不能为了旧测试保留 compatibility branch、legacy fallback、wrapper、镜像状态或第二条转换/校验路径。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

结论

本轮完整复核了 3e5da780893278834e5262874d32db4bb0cd03d7...83cc51db3521cfdb241f7640ebde4984a58466d6 的单 commit 增量,并以 8e114779815954d3a62c7e8dd5758d0e076b3111...83cc51db3521cfdb241f7640ebde4984a58466d6 回看完整 PR tree、review threads、设备恢复边界和 TF 上下游协议。最新 commit 正确把同步回读改成 staging buffer + GPU fence,普通 TF 系统恢复为 24B/particle,Death 也开始消费同一份 world trajectory;但新 readback handle 未接入既有 device-loss 生命周期,context restore 后会先轮询失效的 WebGLSync 再看到 isContentLost,加上 Initial 曲线时域、wrapped shrink、动态 cursor 和稳态分配等历史 P1 仍未处理,当前不适合合入。

阻塞级别:P1。实际 review 动作:REQUEST_CHANGES。目标 HEAD:83cc51db3521cfdb241f7640ebde4984a58466d6。该 HEAD 的 build×3、lint、codecov/project、codecov/patch 和 e2e×4 均已通过;自动 CR 不替代人工 Reviewer 的合入门禁,最终仍需人工审核确认。

已关闭问题清单

  • Current 模式未实现:7dd24d0ac 已通过 TF/shader 路径和公开行为测试闭环。
  • Loop Burst 跨周期、脚本 emit() 命令丢失:分别由 be5bc7bb8af9f5de21 修复。
  • 跨 Scene 配置与迁移崩溃:1fc541fbd 在配置边界拒绝,a7f732e28 删除热路径 throw、迁移后优雅跳过。
  • 每帧重建依赖拓扑:0213a3c75 改为拓扑缓存、入口标脏和 scratch 复用。
  • orbital 积分行为未说明:PR 描述已明确记录 orbit/radial/linear 合成顺序变化。
  • 任意 Birth/Death slot 空闲帧都回读、普通系统 Initial Constant 入口缺失:db59b0830 已缩小回读触发范围并补齐 Entity velocity 入口。
  • 稀疏 Birth 跨帧弦速度失真、Birth 静默忽略 ParticleSubEmitterInheritProperty.Velocity9f0271349 已改由 TF 产出本帧轨迹速度并恢复 direction-only 继承。
  • ParticleGenerator.inheritVelocity 字段缺少 TSDoc:3e5da7808 合入的新 clone 基础设施已补齐字段说明。
  • 同帧 getBufferSubData() 同步 stall、普通 TF 系统承担 48B feedback:83cc51db3 已改成 GPU copy + fence 后回读,并按 RENDERER_TRAJECTORY_FEEDBACK 把普通系统恢复为 24B。
  • Death 绕过权威轨迹、slot inheritVelocity 永远无效:83cc51db3 已直接读取 offset 6..11 的 world position/trajectory velocity,并把同一 velocity 同时交给方向和 slot scale;旧 offset 0..5 + rotation 转换已删除。

问题

  • [P1] ParticleGenerator.ts:389,483-494 / GLBuffer.ts:145-171 / ResourceManager.ts:282-296 — pending readback fence 在设备恢复后先于 isContentLost 被轮询,会让首个恢复帧抛异常

    触发链路是:某帧 _finalizeFeedbackReadback() 创建 WebGLSync,随后 WebGL context 在 fence 消费前 lost/restored。ResourceManager 会把所有 GraphicsResource._isContentLost 置 true 并重建 buffer;但 context loss 已使旧 sync object 失效。首个恢复帧进入 _update() 后,第 389 行先调用 _consumeFeedbackReadback()GLBufferReadback.isReady() 对旧 sync 执行 clientWaitSync(),规范结果是 WAIT_FAILED,当前代码随即 throw;真正用于取消 readback 并清空粒子的 isContentLost 分支要到 483 行才执行,永远到不了。

    应保留 Engine/GraphicsResource.isContentLost 为设备生命周期唯一 owner:在轮询 fence 前从这份既有事实取消并销毁 pending request,再走现有 content-lost 清理;不要新增第二个 device-generation 镜像状态。补“已有 pending fence → device lost/restored → 首帧不调用旧 fence/getData、generator 可继续更新”的设备边界回归测试。

  • [P1] InheritVelocityModule.ts:99-112 / ParticleGenerator.ts:1115-1138Initial Curve/TwoCurves 仍在出生时按 emission timeline 烘焙一次

    _getInitialVelocity(normalizedEmitAge) 对所有曲线模式都在 spawn 时求值并改写 direction/startSpeed。Unity 只让 Constant/TwoConstants 走 ApplyOnBirth();Initial Curve/TwoCurves 保存出生时 emitter velocity,并在 child lifetime 按 particle normalized age 持续求曲线。当前 0→1 曲线会固定在 system/sub-emitter emission time 的单个值,子粒子出生后不再变化。

    应保留 InheritVelocityModule 为 Entity velocity 唯一 owner:Constant/TwoConstants 可出生时烘焙;Curve/TwoCurves 保存 birth emitter velocity 与 per-particle random,在 child update 路径求值。按公开 update 链路补 Initial Curve/TwoCurves 反向测试,不要增加只服务旧测试的 wrapper 或第二套求值入口。

  • [P1] ParticleGenerator.ts:739-769 / SubEmittersModule.ts:429-441 — wrapped shrink 仍只迁移尾段,前段 instance、feedback 与 emission cursor 一起丢失

    环绕存活区间是 [firstRetiredElement, capacity) + [0, firstFreeElement);shrink 的 firstRetiredElement > firstFreeElement 分支仍只 copy/map 第一段。异步 readback 期间只会推迟 resize,pending request 消费后仍进入这份不完整迁移,前段存活粒子会消失或与 cursor 错配。

    应让 _resizeInstanceBuffer() 生成一份覆盖两个 segment 的权威迁移计划,再机械驱动 instance、feedback 和 runtime-state copy,删除三处独立区间 ownership。补 wrap 后收缩 maxParticles、断言两段粒子与 emission cursor 都保留的公开回归测试。

  • [P1] SubEmittersModule.ts:260-268 / EmissionRuntimeState.ts:31-47 — 动态重建 slot state 仍把权威 Rate cursor reset 到 0

    给存活父粒子新增/替换 Birth slot 时,代码把 previousEmissionParentAge 设为当前父龄,却执行 state.emission.reset(seed, 0)。异步 position initialization 只延迟一次处理,并未重置 frameRateTime;下一有效区间 _emitByRateOverTime() 仍会从 0 追赶到当前 emission time,例如父粒子已活 0.5s、rate=10 时单帧补发约 5 个不存在的历史样本。

    应保留 EmissionRuntimeState.frameRateTime 为唯一 Rate 游标 owner:采样 start delay 后,把重建 cursor 设为当前 delay-adjusted emission time,non-loop 时 clamp 到 duration。补“给存活父粒子动态添加 Birth slot,下一帧不补历史”的公开反向测试。

  • [P1] SubEmittersModule.ts:213-240,418-421 / ParticleGenerator.ts:1417-1458 / ParticleSystemManager.ts:63-90 — 稳态仍按父粒子和 emission sample 线性制造 GC

    最新 commit 复用了 staging buffer 和 birth range entries,这是净减法;但每次父粒子 Birth × slot 仍新建 BirthSubEmitterRuntimeState、内部 EmissionRuntimeState/Rand/Vector,退休后整个 ring slot 置 null;每个 sample 又新建 command 和最多六个 Vector/Color,manager clear() 后 per-target array 也失去复用。新异步路径还为每个 readback batch 新建一个 GLBufferReadback wrapper,整体分配量仍随父粒子出生量和子 sample 数线性增长。

    应让 ring slot 长期持有并 reset runtime state,让 ParticleSystemManager 长期持有可复用 target buffer/command entry,generator 只写值;删除 retire -> null -> new、逐 slot new Rand 与逐 sample new command 三条稳态路径。用对象身份、allocation counter 或真实 allocation benchmark证明 warm-up 后分配不再随 sample 数增长。

  • [P2] particle/index.ts:17 / EmissionRuntimeState.ts:12 / RateOverTimeReplay.test.ts:149-164 — 内部 cursor 仍为测试扩大公共 API

    EmissionRuntimeState 的生产 owner 只有 emission/sub-emitter 内部链路,却继续从 public barrel 导出,测试再直接调用 _getEmissionSamples()。应删除 public export,把 Burst 边界契约改由 generator/update 的公开行为验证;不要保留 public wrapper、compat export 或第二采样入口喂旧测试。

  • [P2] ParticleGenerator.ts:1447-1458 — 无 manager 的同步直发仍是第二条调度状态机

    支持路径中的 Birth/Death 都由已注册 renderer 的 scene manager update 产生;同步 fallback 绕过拓扑顺序、命令 owner 和目标 update clock,最新 commit 又为它复制了一份 emissionTime → targetPlayTime 换算。应保留 ParticleSystemManager 为唯一命令调度与时钟换算 owner并删除 fallback,相关测试迁移到 scene/engine update。

  • [P2] SubEmittersModule.ts:98-130 / InheritVelocityModule.ts:11 — 剩余 public surface 的 TSDoc 仍不完整

    addSubEmitter() 返回可继续配置 inheritVelocitySubEmitter,但缺少 @returns;public InheritVelocityModule 类仍使用单行块,偏离同包 EmissionModuleVelocityOverLifetimeModuleColorOverLifetimeModule 等公开模块的多行 TSDoc 约定。请补完整多行 TSDoc,并明确返回 slot 是后续配置 per-slot velocity scale 的入口。

架构、熵增与测试治理

上游一层,Engine/GraphicsResource.isContentLost 应唯一拥有设备有效性,ParticleSystemManager 应唯一拥有拓扑更新、命令消费和跨延迟时钟换算,EmissionRuntimeState 应唯一拥有 Rate/Burst/Distance cursor,InheritVelocityModule 应唯一拥有 Entity velocity;下游一层,TF buffer 应唯一拥有父粒子的 world position/trajectory velocity,staging/fence 只异步搬运该事实,SubEmittersModule 只把 emission plan 与事实组装为命令,目标 generator 只负责 spawn。

本轮从“同帧同步回读 + 所有 TF 48B + Death 平行转换”收敛为“按需 48B trajectory + staging/fence + Birth/Death 共用轨迹协议”,owner 和带宽方向是净减法;为保留异步事件时间新增的 engine-time snapshot 有明确来源,也没有增加 CPU 轨迹镜像。但 readback handle 没有消费既有 device-loss owner,形成了失效资源仍被轮询的生命周期断层;同步 fallback、public runtime cursor、逐粒子 runtime state 和逐 sample command 仍是平行 ownership,应按上文删除而不是再加同步层。

测试方面,该 HEAD 完整 CI 全绿;新增 fence-not-ready、静态 Distance 异步、24B/48B payload 和真实 e2e 异步等待能反向覆盖本轮主路径。缺口是 pending fence 的 device-loss/restore 边界完全未覆盖;Death 现已切到 world trajectory,但现有 World-space Velocity inherit rotates feedback velocity by the spawn rotation 仍按被删除的 offset 0..5 + spawn-rotation 旧协议叙述,且没有 Death orbital、Entity motion、slot curve 的公开反向测试;Initial 曲线、wrapped shrink、动态重建 backlog、稳态 allocation 也仍无公开守护。权威路径替换后应删除旧协议叙述并按新公开契约重写测试,绝不能为了旧测试保留 compatibility branch、legacy fallback、wrapper、镜像状态或第二条转换/校验路径。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants